﻿﻿﻿using System;
﻿using System.Collections.Generic;
﻿using System.IO;
using System.Linq;
﻿using System.Text;
﻿using System.Text.RegularExpressions;
﻿﻿using System.Threading;
﻿﻿using System.Threading.Tasks;
﻿﻿using CSharpTest.Net.IO;
﻿﻿using SearchInFiles.Properties;

namespace SearchInFiles
{
    internal enum State
    {
        Undefined,
        Stopped,
        Error,
        NothingFound,
        Success
    }

    [Flags]
    internal enum SearchType
    {
        Undefined,
        NamesAndContent,
        Names,
        Content
    }

    [Flags]
    internal enum MatchType
    {
        Undefined,
        FileName,
        DirectoryName,
        Content
    }

    internal class ErrorEventArgs
    {
        public string Message { get; private set; }

        public ErrorEventArgs(string message)
        {
            Message = message;
        }
    }

    /// <summary>
    /// Соответствие писковому запросу
    /// </summary>
    internal struct Match
    {
        public int Index { get; private set; }
        public int Length { get; private set; }

        public Match(int index, int length)
            : this()
        {
            Index = index;
            Length = length;
        }
    }

    /// <summary>
    /// Коллекция соответствий в одной строке
    /// </summary>
    internal class MatchCollection
    {
        public IEnumerable<Match> Matches { get; private set; }
        public string Path { get; private set; }
        public MatchType MatchType { get; private set; }
        public int? LineNumber { get; private set; }
        public string MatchedText { get; private set; }

        public MatchCollection(IEnumerable<Match> matches,
                               string filePath,
                               int? lineNumber,
                               string matchedText,
                               MatchType matchType)
        {
            Matches = matches;
            Path = filePath;
            MatchType = matchType;
            LineNumber = lineNumber;
            MatchedText = matchedText;
        }

        public MatchCollection(Match match, string filePath, int? lineNumber, string matchedText, MatchType matchType)
        {
            Matches = new[] {match};
            Path = filePath;
            MatchType = matchType;
            LineNumber = lineNumber;
            MatchedText = matchedText;
        }

        public Match this[int index]
        {
            get { return Matches.ElementAt(index); }
        }
    }

    internal class MatchesFoundEventArgs
    {
        public MatchCollection Matches { get; private set; }

        public MatchesFoundEventArgs(MatchCollection matches)
        {
            Matches = matches;
        }
    }

    internal class Searcher
    {
        public event ErrorEventHandler Error;
        public event MatchesFoundEventHandler MatchesFound;

        public delegate void ErrorEventHandler(object sender, ErrorEventArgs e);

        public delegate void MatchesFoundEventHandler(object sender, MatchesFoundEventArgs e);

        public Func<bool> CheckCancellation { private get; set; }
        public Action<int> ReportProgress { private get; set; }
        public State State { get; private set; }

        private int Percentage
        {
            get
            {
                return _allFilesCount == 0
                           ? 0
                           : Convert.ToInt32((++_processedFilesCount / (decimal) _allFilesCount) * 100);
            }
        }

        private int _allFilesCount;
        private int _processedFilesCount;
        private bool _found;
        private bool _hasFileTypeFilters;
        private static object _lock = new object();

        private static readonly string[] NoFilter = new[] {"*"};

        public int AllFilesCount
        {
            get { return _allFilesCount; }
        }

        public int ProcessedFilesCount
        {
            get { return _processedFilesCount; }
        }

        private void OnError(ErrorEventArgs e)
        {
            State = State.Error;
            var handler = Error;
            if (handler != null)
            {
                handler(null, e);
            }
        }

        private void OnMatchesFound(MatchesFoundEventArgs e)
        {
            _found = true;
            var handler = MatchesFound;
            if (handler != null)
            {
                handler(null, e);
            }
        }

        private void OnReportProgress()
        {
            var handler = ReportProgress;
            if (handler != null)
            {
                handler(Percentage);
            }
        }

        private HashSet<string> _scannedDirectories;

        private struct StringCompare
        {
            public StringCompare(string s, bool ignoreCase)
                : this()
            {
                String = s;
                IgnoreCase = ignoreCase;
            }

            public string String { get; private set; }
            public bool IgnoreCase { get; private set; }
        }

        private static IEnumerable<Match> MatchRegex(string src, Regex regex)
        {
            return
                regex.Matches(src)
                     .Cast<System.Text.RegularExpressions.Match>()
                     .Select(x => new Match(x.Index, x.Length));
        }

        private static IEnumerable<Match> MatchString(string src, StringCompare expr)
        {
            return
                src.AllIndexesOf(expr.String, expr.IgnoreCase)
                   .Select(x => new Match(x, expr.String.Length));
        }

        public void Process(string[] paths,
                            bool useRegex,
                            bool ignoreCase,
                            string[] searchExpressions,
                            string[] fileTypeFilters,
                            SearchType searchType)
        {
            if (paths == null || !paths.Any())
            {
                OnError(new ErrorEventArgs(ErrorResources.AbsentRoot));
            }
            else
            {
                State = State.Undefined;
                _processedFilesCount = 0;
                _allFilesCount = 0;
                _found = false;
                _scannedDirectories = new HashSet<string>();
                if (fileTypeFilters == null || !fileTypeFilters.Any())
                {
                    fileTypeFilters = NoFilter;
                    _hasFileTypeFilters = false;
                }
                else
                {
                    _hasFileTypeFilters = true;
                }
                Task.Factory.StartNew(() => ParallelProcess(paths, fileTypeFilters, path => Interlocked.Increment(ref _allFilesCount), false));
                if (searchExpressions == null)
                {
                    ParallelProcess(paths, fileTypeFilters, ProcessFile);
                }
                else
                {
                    var searchNames = searchType == SearchType.Names || searchType == SearchType.NamesAndContent;
                    var searchContent = searchType == SearchType.Content || searchType == SearchType.NamesAndContent;
                    if (useRegex)
                    {
                        var opts = RegexOptions.IgnorePatternWhitespace |
                                   RegexOptions.Singleline;
                        if (ignoreCase)
                        {
                            opts = opts | RegexOptions.IgnoreCase;
                        }
                        var regexExpressions = searchExpressions.Select(v => new Regex(v, opts));
                        ParallelProcess(paths,
                                        fileTypeFilters,
                                        path =>
                                        ProcessFile(path,
                                                    regexExpressions.AsParallel(),
                                                    MatchRegex,
                                                    searchNames,
                                                    searchContent));
                    }
                    else
                    {
                        var compareExpressions = searchExpressions.Select(v => new StringCompare(v, ignoreCase));
                        ParallelProcess(paths,
                                        fileTypeFilters,
                                        path =>
                                        ProcessFile(path,
                                                    compareExpressions.AsParallel(),
                                                    MatchString,
                                                    searchNames,
                                                    searchContent));
                    }
                }
                if (State == State.Undefined)
                {
                    State = _found ? State.Success : State.NothingFound;
                }
            }
        }

        private void ParallelProcess(IEnumerable<string> paths,
                                     IEnumerable<string> fileTypeFilters,
                                     Action<string> function,
                                     bool sendWrongPathError = true)
        {
            Parallel.ForEach(paths, path =>
                {
                    if (Directory.Exists(path))
                    {
                        Parallel.ForEach(fileTypeFilters,
                                         searchPattern =>
                                             {
                                                 if (CheckCancellation())
                                                 {
                                                     State = State.Stopped; //стоп рекурсивных вызовов
                                                     return;
                                                 }
                                                 var fcounter = new FindFile(path, searchPattern, true, false, true)
                                                     {
                                                         RaiseOnAccessDenied = false
                                                     };
                                                 fcounter.FileFound += (o, e) =>
                                                     {
                                                         if (CheckCancellation())
                                                         {
                                                             State = State.Stopped; //стоп рекурсивных вызовов
                                                             e.CancelEnumeration = true;
                                                         }
                                                         else
                                                         {
                                                             function(e.FullPath);
                                                         }
                                                     };
                                                 fcounter.Find();
                                             });
                    }
                    else if (File.Exists(path))
                    {
                        function(path);
                    }
                    else if (sendWrongPathError)
                    {
                        var err = string.Format(ErrorResources.WrongPath, path);
                        OnError(new ErrorEventArgs(err));
                    }
                });
        }

        private void ProcessFile<T>(string filePath,
                                    ParallelQuery<T> expressions,
                                    Func<string, T, IEnumerable<Match>> matchFunction,
                                    bool searchNames,
                                    bool searchContent)
        {
            if (searchNames)
            {
                ProcessDirectory(Path.GetDirectoryName(filePath), expressions, matchFunction);
                ProcessName(Path.GetFileName(filePath), filePath, MatchType.FileName, expressions, matchFunction);
            }
            if (searchContent)
            {
                var lineNumber = 0;
                if (_hasFileTypeFilters || Utilities.IsText(Path.GetExtension(filePath))) //Если не ищем в конкретном типе файлов, то будем проверять, что расширение зареистрировано в списк текстовых файлов
                    try
                    {
                        using (var sr = new StreamReader(filePath, Encoding.Default))
                        {
                            string line;
                            while ((line = sr.ReadLine()) != null)
                            {
                                if (CheckCancellation())
                                {
                                    return;
                                }
                                if (!string.IsNullOrWhiteSpace(line))
                                {
                                    var matchEnumerable = expressions.SelectMany(expression => matchFunction(line, expression)).OrderBy(match => match.Index);
                                    if (matchEnumerable.Any())
                                    {
                                        OnMatchesFound(
                                            new MatchesFoundEventArgs(
                                                new MatchCollection(matchEnumerable, filePath, lineNumber, line, MatchType.Content)));
                                    }
                                }
                                lineNumber++;
                            }
                        }
                    }
                    catch (IOException)
                    {
                    }
            }
            OnReportProgress();
        }

        private void ProcessFile(string filePath)
        {
            var fileName = Path.GetFileName(filePath);
            OnMatchesFound(
                new MatchesFoundEventArgs(
                    new MatchCollection(
                        new Match(0, fileName.Length), filePath, null, fileName, MatchType.FileName)));
            OnReportProgress();
        }

        private void ProcessDirectory<T>(string directoryPath, ParallelQuery<T> expressions,
                                         Func<string, T, IEnumerable<Match>> matchFunction)
        {
            lock (_lock)
            {
                if (_scannedDirectories.Contains(directoryPath)) return;
                _scannedDirectories.Add(directoryPath);
            }
            var directoryName = Path.GetFileName(directoryPath);
            if (string.IsNullOrEmpty(directoryName)) return;
            ProcessName(directoryName, directoryPath, MatchType.DirectoryName, expressions, matchFunction);
            var parentDirectoryPath = Path.GetDirectoryName(directoryPath);
            if (string.IsNullOrEmpty(parentDirectoryPath)) return;
            ProcessDirectory(parentDirectoryPath, expressions, matchFunction);
        }

        private void ProcessName<T>(string name, string path, MatchType matchType, ParallelQuery<T> expressions,
                                    Func<string, T, IEnumerable<Match>> matchFunction)
        {
            var matchEnumerable = expressions.SelectMany(expression => matchFunction(name, expression)).OrderBy(match => match.Index);
            if (matchEnumerable.Any())
            {
                OnMatchesFound(new MatchesFoundEventArgs(new MatchCollection(matchEnumerable, path, null, name, matchType)));
            }
        }
    }
}